<?php

/**
 * Browser Plugin
 *
 * This plugin provides a html representation, so that a WebDAV server may be accessed
 * using a browser.
 * 
 * This implementation provides for:
 * - The viewing and manipulation of ACLs and properties.
 * - The creation of calendars.
 *
   * Copyright © 2010 Jack Cleaver
   * 
   * This plugin provides functionality added by CalDAV (RFC 4791)
   * It implements new reports, and the MKCALENDAR method.
   *
   * This file is part of Slash.
   *
   * Slash is free software: you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   *
   * Slash is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *
   * You should have received a copy of the GNU General Public License
   * along with Slash.  If not, see <http://www.gnu.org/licenses/>.
 */
class Slash_Browser_Plugin extends Sabre_DAV_ServerPlugin 
{

	/**
	 * reference to server class
	 *
	 * @var Sabre_DAV_Server
	 */
	protected $server;

	/**
	 * enableEditing
	 *
	 * @var bool
	 */
	protected $enablePost = true;

	/**
	 * Creates the object.
	 *
	 * By default it will allow file creation and uploads.
	 * Specify the first argument as false to disable this
	 *
	 * @param bool $enablePost
	 * @return void
	 */
	public function __construct($enablePost=true) 
	{
		$this->enablePost = $enablePost;
	}

	/**
	 * Initializes the plugin and subscribes to events
	 *
	 * @param Sabre_DAV_Server $server
	 * @return void
	 */
	public function initialize(Sabre_DAV_Server $server) 
	{
		$this->server = $server;
		$this->server->subscribeEvent('beforeMethod',array($this,'httpGetInterceptor'), 5);
		if ($this->enablePost) 
		{
			$this->server->subscribeEvent('unknownMethod',array($this,'httpPOSTHandler'));
		}
	}

	/**
	 * This method intercepts GET requests that have the query string "admin", and returns 
	 * an admin HTML page.
	 *
	 * @param string $method
	 * @return bool
	 */
	public function httpGetInterceptor($method) 
	{

		if ($method!='GET') 
		{
			return true;
		}

		$qs = $this->server->httpRequest->getQueryString();
		//Slash_Debug::debug("Caught query string: $qs");
		if ($qs != "admin") 
		{
			return true;
		}

		$this->server->httpResponse->sendStatus(200);
		$this->server->httpResponse->setHeader('Content-Type','text/html; charset=utf-8');
		
		$path = $this->server->getRequestUri();
		$parts = explode("?", $path);
		//print $parts[0];
		
		$index = $this->generateDirectoryIndex($parts[0]);
		$this->server->httpResponse->sendBody($index);

		return false;
	}

	/**
	 * Handles POST requests for tree operations
	 *
	 * This method is not yet used.
	 *
	 * @param string $method
	 * @return bool
	 */
	public function httpPOSTHandler($method) 
	{

		if ($method!='POST')
		{
			return true;
		}
		if (isset($_POST['action'])) switch($_POST['action']) 
		{

			case 'mkcol' :
				if (isset($_POST['name']) && trim($_POST['name'])) 
				{
					// Using basename() because we won't allow slashes
					list(, $folderName) = Sabre_DAV_URLUtil::splitPath(trim($_POST['name']));
					$this->server->createDirectory($this->server->getRequestUri() . '/' . $folderName);
				}
				break;
			case 'put' :
				if ($_FILES) 
				{
					$file = current($_FILES);
				}	else {
					break;
				}
				$newName = trim($file['name']);
				list(, $newName) = Sabre_DAV_URLUtil::splitPath(trim($file['name']));
				if (isset($_POST['name']) && trim($_POST['name']))
				{
					$newName = trim($_POST['name']);
				}

				// Making sure we only have a 'basename' component
				list(, $newName) = Sabre_DAV_URLUtil::splitPath($newName);

				 
				if (is_uploaded_file($file['tmp_name'])) 
				{
					$parent = $this->server->tree->getNodeForPath(trim($this->server->getRequestUri(),'/'));
					$parent->createFile($newName,fopen($file['tmp_name'],'r'));
				}
		}
		$this->server->httpResponse->setHeader('Location',$this->server->httpRequest->getUri());
		return false;

	}

	/**
	 * Escapes a string for html.
	 *
	 * @param string $value
	 * @return void
	 */
	public function escapeHTML($value) 
	{
		return htmlspecialchars($value,ENT_QUOTES,'UTF-8');
	}
	

	/*
	 * The supported privs will be displayed as a tree of checkboxes, 
	 * with abstract privileges grayed.
	 */
	private function appendSP($spaces, $appendTo, $privName, $inner)
	{
		$description = $inner["description"];
		if (!$inner["abstract"])
		{
			$appendTo .= "$spaces<input type='checkbox' value=\"$privName\"> $privName $description<br />\n";
		}
		if (isset($inner["container"]))
		{
			foreach ($inner["container"] as $privName=>$inner)
			{
				$this->appendSP($spaces . "&nbsp;", &$appendTo, $privName, $inner);
			}
		} else {
			$container = null;
		}		
	}
	
	private function makeSelect($name, $data, $princStr)
	{
		$select = "<select name=\"$name\">";
		foreach ($data as $ace)
		{
			$uri = $ace["uri"];
			$name = $ace["name"];
			if ($uri == $princStr)
			{
				$select .= ("<option selected='true' value=\"$uri\">$name</option>\n");
			} else {
				$select .= ("<option value=\"$uri\">$name</option>\n");
			}
		}
		return $select;
	}
		

	/**
	 * Generates the html directory index for a given url.
	 * Access Control:
	 * We get a list of all the principals, and a tree of all 
	 * the privileges. We go through the ACL, and for each principal 
	 * we determine their privs.
	 * If a principal has no ACE on the resource at all, he may yet have
	 * privileges as authenticated:, or through a group membership or something 
	 * like that.
	 * You can't just "add" privs to a principal's ACE, because the ACL is evaluated
	 * sequentially. You have to insert a grant or deny at a specific point in the list,
	 * or delete one. You can't delete a protected ACE.
	 * So we display a list of all the ACEs, in order. That's $aceData. Each ACE
	 * has controls for deleting and inserting. Insert creates a new row in the table, 
	 * and allows you to select (a) a principal, (b) a privilege, and (c) whether it is 
	 * to be granted or denied. 
	 * 
	 * OK, so we create a set of resources for an HTML page to refer to, and then we 
	 * include a page appropriate to the type of the resource. The page will extract the
	 * information it needs from the resources.
	 *  
	 * The types (and things you can change) are:
	 * - File (owner, ACL, displayname)
	 * - Directory (owner, ACL, displayname)
	 * - Calendar (owner, ACL, displayname)
	 * - CalendarObject ()
	 * - Principal (owner, ACL, displayname, password)
	 * - Principal Collection (displayname)
	 * 
	 * 
	 *
	 * @param string $path
	 * @return string
	 */
	public function generateDirectoryIndex($path)
	{
		$files = $this->server->getPropertiesForPath($path,array(
        '{DAV:}resourcetype',
        '{DAV:}getcontenttype',
        '{DAV:}getcontentlength',
        '{DAV:}getlastmodified',
        '{DAV:}acl',
				"{DAV:}supported-privilege-set",
				"{DAV:}owner"
				),1);
		
		$fileHtml = "";
		//var_dump($files);
		$aceData = "";
		$princs = $this->server->getAllPrincipals();
		
		//selpData is data for the Principal select element.
		$selpData = array();
		foreach ($princs as $princ)
		{
			$uriNode = new Slash_ACL_FSExt_UriNode($princ);
			$uri = $uriNode->getUri();			
			$name = $princ->getName();
			$selpData[] = array("uri"=>"href:$uri", "name"=>$name);
		}
		$selOwnerData = array() + $selpData;
		
		$selpData[] = array("uri"=>"authenticated:", "name"=>"authenticated");
		$selpData[] = array("uri"=>"unauthenticated:", "name"=>"unauthenticated");
		$selpData[] = array("uri"=>"self:", "name"=>"self");
		$selpData[] = array("uri"=>"all:", "name"=>"all");
		$selpData[] = array("uri"=>"property:{DAV:}owner", "name"=>"owner");
		$selp="";
		$spsData = "";
		$privData = "";
		foreach($files as $k=>$file) 
		{
			//$aceData .= $file["href"];
			if (rtrim($file['href'],'/')==$path)
			{
				//This is the target resource
				$resourcename = $file["href"];
				$resourcetype = $file[200]["{DAV:}resourcetype"]->getValue();
				//The resourcetype is either a string or an array of string.
				//$aceData .= "Found the current directory<br />";

				$owner = $file[200]["{DAV:}owner"]->getHref();
				$selOwner = $this->makeSelect("selo", $selOwnerData, "href:$owner");
				
				if (isset($file[200]["{DAV:}acl"]))
				{
					$acl = $file[200]["{DAV:}acl"];
					$sps = $file[200]["{DAV:}supported-privilege-set"];
					$aces = $acl->getAces();
					foreach ($aces as $princStr=>$ace)
					{
						//You could have (e.g.) grant all, deny write, grant write-content for a single principal.
						foreach ($ace as $aceAction)
						{
							$select = $this->makeSelect("sel1", $selpData, $princStr);
							$aceData .= "<tr>";
							$aceData .= "<td>$select</td>";
							$p = $aceAction["protected"]?"P":"U";
							$g = $aceAction["grant"]?"G":"D";
							$aceData .= "<td>$p</td>";
							$aceData .= "<td>$g</td>";
							$aceData .= "<td>";
							foreach ($aceAction["privs"] as $priv)
							{
								$aceData .= "$priv ";
							}
							$aceData .= "</td></tr>\n";
						}
					}
					$aceData .= "</table>";
					$spsData = "<form>";
					$privileges = $sps->getPrivileges();
					foreach ($privileges as $privname=>$inner)
					{
						$this->appendSP("", &$spsData, $privname, $inner);
					}
					$spsData .= "</form>";
				}				
			} else {
				list(, $name) = Sabre_DAV_URLUtil::splitPath($file['href']);
				$name = $this->escapeHTML($name);
	
				$type = null;
				if (isset($file[200]['{DAV:}resourcetype'])) 
				{
					$type = $file[200]['{DAV:}resourcetype']->getValue();
					// resourcetype can have multiple values
					if (is_array($type)) 
					{
						$type = implode(', ', $type);
					}
	
					// Some name mapping is preferred
					switch($type) 
					{
						case '{DAV:}collection' :
							$type = 'Collection';
							break;
					}
				}
	
				// If no resourcetype was found, we attempt to use
				// the contenttype property
				if (!$type && isset($file[200]['{DAV:}getcontenttype'])) 
				{
					$type = $file[200]['{DAV:}getcontenttype'];
				}
				if (!$type) 
				{
					$type = 'Unknown';
				}
	
				$type = $this->escapeHTML($type);
				$size = isset($file[200]['{DAV:}getcontentlength'])?(int)$file[200]['{DAV:}getcontentlength']:'';
				$lastmodified = isset($file[200]['{DAV:}getlastmodified'])?$file[200]['{DAV:}getlastmodified']->getTime()->format(DateTime::ATOM):'';
	
				$fullPath = Sabre_DAV_URLUtil::encodePath('/' . trim($this->server->getBaseUri() . ($path?$path . '/':'') . $name,'/'));
	
				$fileHtml.= "<tr>
	<td><a href=\"{$fullPath}\">{$name}</a></td>
	<td>{$type}</td>
	<td>{$size}</td>
	<td>{$lastmodified}</td>
	</tr>";
			}

		}
		$dirname = $this->escapeHTML($path);
		ob_start();
		if (!is_array($resourcetype))
		{
			if ($resourcetype == "{DAV:}collection")
			{
				include "collection.php";
			} else {
				include "file.php";
			}
		} else if (in_array("{DAV:}collection", $resourcetype))
		{
			if (in_array("{urn:ietf:params:xml:ns:caldav}calendar", $resourcetype))
			{
				include "calendar.php";
			} else if (in_array("{DAV:}principal", $resourcetype))
			{
				include "principal.php";
			} else {
				include "collection.php";
			}
		}
		$html = ob_get_clean();
		return $html;
	}

}
